﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Dynamic;
using NLog;
using Raven.Client;
using Raven.Client.Document;
using Raven.Client.Extensions;
using Raven.Client.Indexes;

namespace TimeTracker.Persistence
{
    public abstract class DatabaseContext : IDisposable
    {
        private static readonly object syncRoot = new object();
        private static readonly Logger logger = LogManager.GetCurrentClassLogger();

        private readonly dynamic descriptor;

        public string Name { get { return descriptor.Name; } }
        public string InstanceName { get { return descriptor.InstanceName; } }
        public IDocumentStore Store { get { return descriptor.Store; } }

        private Lazy<IDocumentSession> database;
        public IDocumentSession Database { get { return database.Value; } }

        private static readonly IDictionary<string, dynamic> databases;
        static DatabaseContext()
        {
            // Static constructors are guaranteed to be run by the CLR in serialized non-threaded form.
            var localDatabases = AppDomain.CurrentDomain.GetData("RavenDatabases");
            if (localDatabases == null)
            {
                localDatabases = new ConcurrentDictionary<string, dynamic>();
                AppDomain.CurrentDomain.SetData("RavenDatabases", localDatabases);
            }

            databases = (ConcurrentDictionary<string, dynamic>)localDatabases;
        }

        protected DatabaseContext(string databaseName)
        {
            database = new Lazy<IDocumentSession>(CreateDocumentSessionInternal);

            if (!databases.TryGetValue(databaseName, out descriptor))
                throw new ArgumentException(string.Format("Database {0} not registered", databaseName), "databaseName");

            logger.Trace(string.Format("Database Session for {0} Opened", Name));
        }

        private IDocumentSession CreateDocumentSessionInternal()
        {
            lock (descriptor)
            {
                if (!descriptor.IsInitialized)
                    ConfigureDatabase();
            }

            return CreateDocumentSession();
        }

        // The bulk of the clean-up code is implemented in Dispose(bool)
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (Database != null)
                {
                    Database.Dispose();
                    logger.Trace(string.Format("Database Session for {0} Closed", Name));
                }

                database = null;

                logger.Trace("DatabaseContext '{0}' disposed", Name);
            }
        }

        protected virtual IDocumentSession CreateDocumentSession()
        {
            return Store.OpenSession();
        }

        protected virtual void ConfigureDatabase()
        { }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        // NOTE: Leave out the finalizer altogether if this class doesn't 
        // own unmanaged resources itself, but leave the other methods
        // exactly as they are. 
        ~DatabaseContext()
        {
            // Finalizer calls Dispose(false)
            Dispose(false);
        }

        public static void RegisterTransient(string databaseName)
        {
            Register(databaseName, databaseName, isTransient: true, instanceName: databaseName);
        }

        public static void RegisterTransient(string databaseName, string instanceName)
        {
            if (string.IsNullOrWhiteSpace(instanceName))
                throw new ArgumentNullException("instanceName");

            Register(databaseName, databaseName, isTransient: true, instanceName: instanceName);
        }

        public static void RegisterTransient(string databaseName, string connectionStringName, string instanceName)
        {
            if (string.IsNullOrWhiteSpace(instanceName))
                throw new ArgumentNullException("instanceName");

            Register(databaseName, connectionStringName, isTransient: true, instanceName: instanceName);
        }

        public static void Register(string databaseName)
        {
            Register(databaseName, databaseName);
        }

        public static void Register(string databaseName, string connectionStringName)
        {
            Register(databaseName, connectionStringName, false, null);
        }

        public static void Register(string databaseName, string connectionStringName, string instanceName)
        {
            Register(databaseName, connectionStringName, false, instanceName);
        }

        private static void Register(string databaseName, string connectionStringName, bool isTransient = false, string instanceName = null)
        {
            if (string.IsNullOrWhiteSpace(databaseName))
                throw new ArgumentNullException("databaseName");
            if (string.IsNullOrWhiteSpace(connectionStringName))
                throw new ArgumentNullException("connectionStringName");

            dynamic descriptor;
            if (!databases.TryGetValue(databaseName, out descriptor))
            {
                lock (syncRoot)
                {
                    if (!databases.TryGetValue(databaseName, out descriptor))
                    {
                        if (string.IsNullOrWhiteSpace(instanceName))
                            instanceName = databaseName;

                        var theStore = new DocumentStore { ConnectionStringName = connectionStringName };

                        if (isTransient)
                        {
                            instanceName += new Random().Next();

                            using (var transientStore = new DocumentStore { ConnectionStringName = connectionStringName }.Initialize())
                            {
                                logger.Trace(string.Format("Transient Database Store {0} Initialized", instanceName));

                                using (var session = transientStore.OpenSession())
                                {
                                    session.Advanced.DatabaseCommands.CreateInMemoryDatabase(instanceName);
                                    session.SaveChanges();

                                    logger.Trace(string.Format("Transient Database {0} Created", instanceName));
                                }
                            }

                            theStore.DefaultDatabase = instanceName;
                        }
                        else
                        {
                            theStore.DefaultDatabase = instanceName;
                        }

                        theStore.Initialize();

                        logger.Trace(string.Format("Database Store {0} Initialized", databaseName));

                        if (!isTransient)
                            theStore.DatabaseCommands.EnsureDatabaseExists(instanceName);

                        dynamic currentDatabase = new ExpandoObject();
                        currentDatabase.Name = databaseName;
                        currentDatabase.Store = theStore;
                        currentDatabase.InstanceName = instanceName;
                        currentDatabase.IsTransient = isTransient;
                        currentDatabase.IsInitialized = false;

                        databases[databaseName] = currentDatabase;
                    }
                }
            }
        }

        public static void Unregister(string databaseName)
        {
            if (string.IsNullOrWhiteSpace(databaseName))
                throw new ArgumentNullException("databaseName");

            dynamic descriptor;
            if (databases.TryGetValue(databaseName, out descriptor))
            {
                DocumentStore store = descriptor.Store;
                string instanceName = descriptor.InstanceName;

                // If is transient
                if (descriptor.IsTransient)
                    store.DatabaseCommands.DeleteDatabase(instanceName);

                store.Dispose();

                databases.Remove(databaseName);
                logger.Trace(string.Format("Database Store {0} Removed", databaseName));
            }
        }

        public static string GetInstanceName(string databaseName)
        {
            if (string.IsNullOrWhiteSpace(databaseName))
                throw new ArgumentException("parameter cannot be null or empty.", "databaseName");

            dynamic descriptor;
            if (!databases.TryGetValue(databaseName, out descriptor))
                throw new ArgumentException(string.Format("Database {0} does not exists.", databaseName), databaseName);

            return descriptor.InstanceName;
        }

        public static bool IsRegistered(string databaseName)
        {
            if (string.IsNullOrWhiteSpace(databaseName))
                throw new ArgumentException("parameter cannot be null or empty.", "databaseName");

            dynamic descriptor;
            return databases.TryGetValue(databaseName, out descriptor);
        }
    }

    public class TimeTrackerContext : DatabaseContext
    {
        public TimeTrackerContext()
            : base("TimeTracker")
        { }

        public TimeTrackerContext(string databaseName)
            : base(databaseName)
        { }

        protected override IDocumentSession CreateDocumentSession()
        {
            var documentStore = (DocumentStore)this.Store;            

            var listeners = new DocumentSessionListeners{ };

            var session = new DocumentSession(documentStore,
                                       listeners,
                                       Guid.NewGuid(),
                                       documentStore.DatabaseCommands,
                                       documentStore.AsyncDatabaseCommands);

            session.Conventions.DefaultQueryingConsistency = ConsistencyOptions.QueryYourWrites;           

            return session;
        }

        protected override void ConfigureDatabase()
        {
            var documentStore = (DocumentStore)this.Store;
            IndexCreation.CreateIndexes(typeof(TimeTrackerContext).Assembly, documentStore);
        }
    }

    public class TimeTrackerBulkContext : TimeTrackerContext
    {
        protected override void ConfigureDatabase()
        {
            base.ConfigureDatabase();

            var documentStore = (DocumentStore)this.Store;

            // Allow bulk importing.
            documentStore.Conventions.MaxNumberOfRequestsPerSession = int.MaxValue;
        }
    }
}
